在之前的三篇文章中,我们以“老古董”的身份,捍卫了模板元编程,反驳了“用 constexpr 就够了”的说法。
但批评者可能会说:“你只是用模板做编译期计算,那你对 ‘其他’ 现代特性怎么看?”
好问题!今天,我们就来 “全面” 解读 “与编译器交流的各种方式”。
从 C++ 最古老的 inline,到 C++23 最新的 [[assume()]]——
这些不是“特性”,它们是 “你和编译器之间的对话”。每一种都是一种
“沟通方式”,一种 “请求”,一种 “命令”,
一种 “信任投票”。
所以,当有人说“你是老古董”时,你可以告诉他:“我不仅知道这些新特性,我还知道它们 ‘怎么跟编译器聊天’。”
inline 是 ‘请求’,[[likely]] 是 ‘建议’,constexpr 是 ‘命令’,[[nodiscard]] 是 ‘警告’,noexcept 是 ‘承诺’,[[assume()]] 是 ‘无条件信任’。inline —— “请求式”交流
inline 是最古老的“编译器对话”。它是 “请求” 的语气:
“亲爱的编译器,如果方便的话,请把这个函数内联一下,好吗?” 编译器通常会说:
“我考虑一下。” 然后它做自己的决定。这是一种 “礼貌的交流”。
inline int add(int a, int b) { return a + b; }
// 程序员:“请内联我。”
// 编译器:“嗯,好,我内联了。哦不,我没内联。看我心情。”
[[likely]] / [[unlikely]] —— “预测式”交流
C++20 加入的 [[likely]] 和 [[unlikely]] 是 “预测” 的语气:
“我觉得这个分支大概率会走。” 编译器会利用这些信息做 “分支预测优化”。
这就像你跟朋友说:“我猜他会来。”——编译器信了你,然后安排座位(预取指令)。
if (x > 0) [[likely]] {
// 程序员:“大概率走这里。”
} else [[unlikely]] {
// 程序员:“不太会走这里。”
}
// 编译器:“好的,我按你说的优化了。但如果你错了,性能下降是你的事。”
__attribute__((hot)) / ((cold)) —— “专横式”交流
GCC/Clang 的 hot 和 cold 扩展是 “专横” 的语气:
“这个函数是热点函数,你给我好好优化!” 编译器不太喜欢这种语气,但大部分时候会照做。
这就像你说:“帮我做这个,马上。”——同事虽然不爽,但还是做了。
__attribute__((hot)) void hot_function() { /* 频繁调用 */ }
__attribute__((cold)) void cold_function() { /* 很少调用 */ }
// 程序员:“这个函数很重要!”
// 编译器:“知道了知道了,别吼。”
restrict —— “承诺式”交流
restrict(C99 中就有,C++ 中通过 __restrict 或编译器扩展)是 “承诺” 的语气:
“我承诺,这个指针不会和其他指针别名。” 编译器基于这个承诺可以做激进优化。
如果你撒谎……“未定义行为” 会找上你。
void add(int* restrict a, int* restrict b, int* restrict c) {
*c = *a + *b;
}
// 程序员:“我发誓它们不会重叠。”
// 编译器:“好,我信你,然后做优化。如果你骗我……呵呵。”
constexpr —— “命令式”交流
constexpr 是 “命令” 的语气:“在编译期求值这个值!”
但编译器会反驳:“我尽量,但如果我做不到(传入了运行时变量),我就只能在运行时算。”
这就像你跟下属说“把这个做完”,下属说“我尽量,但如果资源不够,我就做不了”。
constexpr int square(int n) { return n * n; }
constexpr int x = square(5); // ✅ 编译期算
int y = square(get_input()); // ❌ 运行时算
// 程序员:“编译期算!”
// 编译器:“好的……哦,你传了运行时变量,我只能在运行时算。”
constinit —— “强制命令式”交流
C++20 的 constinit 是 “强制命令” 的语气:
“我不管,这个变量必须在编译期初始化!如果你做不到,就别编译了。”
这比 constexpr 更“强硬”——它只用于变量,且 “必须在编译期初始化”。
constinit int x = 10; // ✅ 必须在编译期初始化
constinit int y = get_random(); // ❌ 编译错误!不能在编译期确定
// 程序员:“我说了算!”
// 编译器:“好的好的,别生气。我照做。”
[[nodiscard]] —— “劝告式”交流
[[nodiscard]] 告诉编译器:“这个返回值很重要,不要忽略它。”
然后编译器会对忽略返回值的调用产生警告。这就像你在产品包装上写:
“请勿吞食”——用户可能会忽略,但你 “警告” 了。
[[nodiscard]] int get_important_value() { return 0xFFFFFFFF; }
get_important_value(); // ⚠️ 警告:忽略返回值
// 编译器:“你确定不要这个值?我提醒你了哦。”
[[maybe_unused]] —— “沉默式”交流
[[maybe_unused]] 告诉编译器:“这个变量可能没用,但我保留它。”
编译器本来会警告“未使用的变量”,但加上这个属性后,编译器就 “闭嘴” 了。
这就像你跟人说:“这个我留着,虽然可能用不上。”
[[maybe_unused]] int temp = 0xCCCCCCCC; // 编译器不会警告“未使用”
// 程序员:“我知道它没用,但我留着。”
// 编译器:“好吧,我不说了。”
[[fallthrough]] —— “意图声明式”交流
在 switch 语句中,[[fallthrough]] 告诉编译器:
“我是故意 case 穿透的,不是忘记写 break。”
于是编译器不会警告你。这就像你说:“我故意的。”
switch (x) {
case 1:
do_something();
[[fallthrough]]; // 故意的!
case 2:
do_something_else();
break;
}
// 程序员:“我故意穿透的,别吵。”
// 编译器:“好吧,我不警告了。”
volatile —— “警示式”交流
volatile 告诉编译器:“这个变量可能被 ‘外部’ 修改。”
于是编译器不会优化它的读写。这就像你说:“这个房间可能有老鼠,进出小心一点。”
volatile int flag = 0;
while (!flag) { /* 等待外部中断修改 flag */ }
// 程序员:“这个变量可能在程序外部被改。”
// 编译器:“好的,我不会缓存它,每次都从内存读。”
noexcept —— “合同式”交流
noexcept 是 “合同” 的语气:“我保证这个函数不会抛出异常。”
编译器基于此可以做优化(比如不生成栈展开代码)。如果你撒谎(真的抛出了异常),
程序会调用 std::terminate——“合同违约” 的后果。
noexcept void safe_function() { /* 不会抛异常 */ }
// 程序员:“我保证不会抛异常。”
// 编译器:“好,我信了。如果被骗了,std::terminate 会来找你。”
[[assume()]] —— “无条件信任式”交流
C++23 加入了 [[assume(expression)]]。这是 “无条件信任” 的语气:
“我 ‘绝对’ 相信这个条件永远为真,编译器你拿去优化吧。”
这是最 “危险” 的交流方式——如果条件为假,程序进入 “未定义行为”。
这就像你说:“我绝对相信你会准时到。”——如果你错了,后果自负。
void process(int* p) {
[[assume(p != nullptr)]]; // 我绝对相信 p 不是空指针
*p = 0xCCCCCCCC; // 编译器会优化掉空指针检查
}
// 程序员:“我赌 p 不是 nullptr。”
// 编译器:“好,我赌了。如果你输了,UB 是你的事。”
[[assume()]] 是最“危险”的交流方式。inline。基本的“请求式”交流。constexpr 和 [[nodiscard]]。
开始“命令”和“警告”编译器了。[[likely]] 和 noexcept。
开始“预测”和“签约”了。restrict 和 volatile。
你开始“承诺”和“警示”了。constinit 和 [[assume()]]。
你开始“强制命令”和“无条件信任”了。[[assume()]] 是最好的交流方式——因为 “它最短”。
你开始在所有地方写 [[assume(true)]],因为“反正编译器不会出错”。
你 “超越” 了信任。但“老古董”的说法是不成立的。一个真正的“老古董”不是“不知道新特性”, 而是 “知道如何用这些特性”——并且知道 “它们不能替代模板元编程”。
所以,下次有人说你“老古董”时,你可以回答:
“我不仅知道 constexpr,我还知道 constinit 和 [[assume()]]。
我只是选择在合适的时候用它们。”
—— 一个“老古董”,但知道所有“新把戏”